优秀的 PDF 阅读器开源库主要有两个:
所以在前端想要做 PDF 阅读器的需求,那 pdf.js 基本可以算是唯一的选择
虽然 pdf.js 很强大、功能很丰富,但是其作为一个底层通用库,对实用功能封装比较少,而且其 API 文档比较简陋,在摸索上手阶段需要耗费不少的功夫,实际使用也需要进行大量开发工作以支持业务
所以在实际项目中,都会对 pdf.js 进行封装或二次开发,当然也有一些封装的比较好的项目:
笔者所在公司现在使用 PDF.js Express,但由于 PDF.js Express 收费昂贵且闭源,现调研转向 ZoteroReader 的可能性和迁移成本
都是基于 pdf.js,差别不大
![[Pasted image 20250724103920.png]]
![[Pasted image 20250724104000.png]]
提供丰富的标注工具:
![[Pasted image 20250724104039.png]]
有限的标注工具,仅有以下工具:
![[Pasted image 20250724104057.png]]
const debouncedSave = debounce(() => {
const annotationManager = webViewerCore?.annotationManager as any
annotationManager
.exportAnnotations({
links: false,
widgets: false,
})
.then((xfdfData: any) => {
if (!libraryId) return
ApiSavePDFNote({
libraryId,
note: xfdfData,
}).catch(() => {
message.error(trans(I18N.libraryV2.saveNoteFailed))
})
})
}, 800)
// 监听笔记修改
useEffect(() => {
if (webViewerCore) {
const { documentViewer, annotationManager } = webViewerCore
// 监听文档加载
documentViewer.addEventListener(
webViewerCore.DocumentViewer.Events.DOCUMENT_LOADED,
() => {
annotationManager.addEventListener('annotationChanged', (annotations, action, { imported }) => {
if (['delete', 'modify', 'add'].includes(action) && !imported) {
if (libraryId) {
debouncedSave()
} else {
bookmark(false)
annotationManager.deleteAnnotations(annotations)
}
}
})
},
{ once: true },
)
}
}, [webViewerCore, libraryId])
// 笔记还原
useEffect(() => {
if (webViewerCore && note) {
const { documentViewer, annotationManager } = webViewerCore
// 监听文档加载
documentViewer.addEventListener(
webViewerCore.DocumentViewer.Events.DOCUMENT_LOADED,
() => {
annotationManager
.importAnnotations(note?.trim())
.then(() => {
message.success({ content: trans(I18N.libraryV2.loadNoteSuccess) })
})
.catch(() => {
message.error({ content: trans(I18N.libraryV2.loadNoteFailed) })
})
},
{ once: true },
)
}
}, [webViewerCore, note])
const reader = iframeWindow.createReader({
// 监听笔记修改
onSaveAnnotations: async function (annotations) {
console.log('Save annotations', annotations)
},
onDeleteAnnotations: function (ids) {
console.log('Delete annotations', JSON.stringify(ids))
},
// 其他配置...
})
// 笔记还原
reader.setAnnotations(annotations)
支持
instance.UI.setLanguage(isChinese ? 'zh_cn' : 'en')
支持,不过官网只有英文,需要开发者编写中文文案并加载
![[Pasted image 20250724104203.png]]
编写 ftl 文件(中文文案),进行加载。源码
![[Pasted image 20250724104428.png]]
// 划词 popup 添加自定义按钮
instance.UI.textPopup.add([
// 翻译
{
type: 'customElement',
render: () => <CustomPopupButton buttonType={EPopupButton.TRANSLATE} instance={instance} onButtonClick={onTranslate} />,
},
// AI 分析
{
type: 'customElement',
render: () => <CustomPopupButton buttonType={EPopupButton.AIANALYSIS} instance={instance} onButtonClick={onAIAnalysis} />,
},
])
![[Pasted image 20250724104446.png]]
也支持自定义
iframeWindow.addEventListener('customEvent', (event: any) => {
if (event?.detail?.type !== 'renderTextSelectionPopup') {
return
}
const { append } = event.detail
append('2342423432')
})
![[Pasted image 20250724104555.png]]
instance.Core.documentViewer.addEventListener('annotationsLoaded', () => {
const { pos, unit } = qs.parse(location.search) as unknown as {
pos: [number, [number, number, number, number]][]
unit: string
}
if (pos?.[0] && unit === 'percent') {
const [page, range] = pos[0]
const newPage = Number(page)
const newRange = range?.map(item => Number(item))
const pageInfo = instance.Core.documentViewer.getDocument().getPageInfo(newPage) || {
width: 0,
height: 0,
}
const x1 = newRange[0] * pageInfo.width
const y1 = newRange[1] * pageInfo.height
const x2 = newRange[2] * pageInfo.width
const y2 = newRange[3] * pageInfo.height
onNavigate({
tl: [x1, y1],
br: [x2, y2],
page: newPage - 1,
})
}
if (pos?.[0] && unit !== 'percent') {
const [page, range] = pos[0]
onNavigate({
tl: [Number(range[0]), Number(range[1])],
br: [Number(range[2]), Number(range[3])],
page: Number(page),
})
}
})
实际就是 PDF 跳转到指定位置 + 在该区域添加一个标注,ZoteroReader 都支持
PDF 内链接跳转、快捷键(如 ctrl+z)、目录预览、搜索、缩放等其他功能都支持